package com.magenta.maxoptra.gis.service;

import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.commons.Logger;
import com.magenta.maxoptra.gis.util.Apis;
import com.magenta.mrs.util.DefaultClient;
import com.magenta.mrs.util.GeoInformationClient;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthState;
import org.apache.http.auth.Credentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.params.AuthPolicy;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.*;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultProxyAuthenticationHandler;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.jetbrains.annotations.Nullable;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.EJB;
import javax.ejb.Local;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.security.Principal;
import java.util.*;

/*
* Project: MaxOptRA v1.2
* User: Ivan Usalko
* Date: 13.09.2009 21:36:37
*
* Copyright (c) 1999-2009 Magenta Corporation Ltd. All Rights Reserved.
* Magenta Technology proprietary and confidential.
* Use is subject to license terms.
*/

@Local({ConnectionPool.class, AccessToGeoProvider.class})
public class BasicConnectionPool implements ConnectionPool, AccessToGeoProvider {

    public static final long BAD_CONNECTION = -1L;

    protected Logger log;

    @EJB
    ProvidersRegistry providersRegistry;

    Map<Long, Object> connections;
    /**
     * Pool of http clients.
     */
    Map<ConnectionKey, GeoInformationClient> clients;

    private Object getGeoProviderClass(GeoProviderSettings connSettings) {
        Object connector = null;

        String providerName = connSettings.getProviderName();
        String versionApi = connSettings.getVersionApi();
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        try {
            Class<?> connectorClass = cl.loadClass(providersRegistry.getImplementor(providerName, versionApi));
            connector = connectorClass.newInstance();
        } catch (ClassNotFoundException e) {
            log.error("WRONG CLASS FOR PROVIDER %s(%s) IN CONNECTION PARAMS", e, providerName, versionApi);
        } catch (IllegalAccessException e) {
            log.error("CANNOT ACCESS TO CLASS FOR PROVIDER %s(%s)", e, providerName, versionApi);
        } catch (InstantiationException e) {
            log.error("CANNOT INSTANTIATE CLASS FOR PROVIDER %s(%s)", e, providerName, versionApi);
        }
        return connector;
    }

    public long getConnectionId(GeoProviderSettings connSettings) {
        final Object connector = getGeoProviderClass(connSettings);
        if (connector == null) {
            return BAD_CONNECTION;
        }

        //TRY TO GET EXISTING CLIENT
        ConnectionKey key = createConnectionKey(connSettings);
        GeoInformationClient geoClient = getGeoInformationClient(connSettings, key);

        setProxySettings(connSettings, geoClient);

        long connectionId = generateId(key);
        IGeoConfigurable existingConnector = (IGeoConfigurable) connections.get(connectionId);
        if (existingConnector == null) {
            ((IGeoConfigurable) connector).setClient(geoClient);
            connections.put(connectionId, connector);
        } else {
            existingConnector.setClient(geoClient);
        }
        return connectionId;
    }

    private ConnectionKey createConnectionKey(final GeoProviderSettings connSettings) {
        final String providerName = connSettings.getProviderName().toLowerCase();
        final long cid = Apis.cid(connSettings.getProviderName(), connSettings.getVersionApi(), connSettings.getCidDiscriminator());
        final String base_url = connSettings.getGisParam(GisParameter.reverse_geocoding_url.withProvider(providerName));
        final String reverse_geocoding_url = connSettings.getGisParam(GisParameter.reverse_geocoding_url.withProvider(providerName));
        final String routing_url = connSettings.getGisParam(GisParameter.routing_url.withProvider(providerName));
        final String geocoding_url = connSettings.getGisParam(GisParameter.geocoding_url.withProvider(providerName));
        final String wsdl_location = connSettings.getGisParam(GisParameter.wsdl_location.withProvider(providerName));
        return new ConnectionKey(cid, base_url, reverse_geocoding_url, routing_url, geocoding_url, wsdl_location);
    }

    private GeoInformationClient getGeoInformationClient(GeoProviderSettings connSettings, ConnectionKey key) {
        if (clients == null) {
            init();
        }

        GeoInformationClient geoClient = clients.get(key);
        if (geoClient == null) {
            //HttpParams params = new BasicHttpParams();
            SchemeRegistry reg = new SchemeRegistry();
            SchemeSocketFactory socketFactory = new PlainSocketFactory();
            Scheme httpScheme = new Scheme("http", 80, socketFactory);
            reg.register(httpScheme);
            PoolingClientConnectionManager connManager = new PoolingClientConnectionManager(reg);
            // Increase max total connection to 200
            connManager.setMaxTotal(200);
            // Increase default max connection per route to 20
            connManager.setDefaultMaxPerRoute(8);
            geoClient = new DefaultClient(connManager, null);
            clients.put(key, geoClient);
        }

        updateGeoClientProperties(connSettings, geoClient);

        return geoClient;
    }

	public static GeoInformationClient createHttpClient() {
		GeoInformationClient geoClient;
		HttpParams params = new BasicHttpParams();
		SchemeRegistry reg = new SchemeRegistry();
		SchemeSocketFactory socketFactory = new PlainSocketFactory();
		Scheme httpScheme = new Scheme("http", 80, socketFactory);
		reg.register(httpScheme);
		ClientConnectionManager connManager = new ThreadSafeClientConnManager(reg);
		geoClient = new DefaultClient(connManager, params);
		return geoClient;
	}

    private void updateGeoClientProperties(GeoProviderSettings connSettings, GeoInformationClient geoClient) {
        final GisParameters clientProperties = geoClient.getProperties();
        final GisParameters gisParameters = connSettings.getGisParams();
        if (clientProperties != null && gisParameters != null) {
            clientProperties.putAll(gisParameters);
        }
    }

	private static class ProxyAuthHandler extends DefaultProxyAuthenticationHandler {

		private static final ProxyAuthHandler instance = new ProxyAuthHandler();

		private static final List<String> SCHEME_PRIORITY =
	        Collections.unmodifiableList(Arrays.asList(new String[] {
	                AuthPolicy.BASIC,
	                AuthPolicy.NTLM,
	                AuthPolicy.SPNEGO,
	                AuthPolicy.DIGEST,
	    }));

		@Override
		protected List<String> getAuthPreferences() {
			return SCHEME_PRIORITY;
		}

		public static ProxyAuthHandler getInstance() {
			return instance;
		}

	}

    private void setProxySettings(GeoProviderSettings connSettings, GeoInformationClient geoClient) {
        final String proxyHost;
        final int proxyPort;
        URL proxyURL = connSettings.getProxyURL();
        if (proxyURL != null) {
            proxyHost = proxyURL.getHost();
            proxyPort = proxyURL.getPort();
        } else {
            proxyHost = null;
            proxyPort = 3128;
        }

        String login = connSettings.getProxyLogin();
        final Credentials proxyCredentials = new ProxyCredentials(login, connSettings.getProxyCredentials());

        //IF PROXY CHANGED REINITIALIZE CONNECTION
        Object oldProxy = geoClient.getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY);
        HttpHost proxy = proxyHost != null ? new HttpHost(proxyHost, proxyPort) : null;
        DefaultHttpClient httpClient = (DefaultHttpClient) geoClient;
        if (proxy == null) {
            geoClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
            httpClient.removeRequestInterceptorByClass(PreemptiveAuth.class);
        } else if (!proxy.equals(oldProxy)) {
            geoClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
            httpClient.getCredentialsProvider().setCredentials(new AuthScope(proxyHost, proxyPort), proxyCredentials);
            BasicHttpContext localContext = new BasicHttpContext();
            //BasicScheme basicAuth = new BasicScheme();
            //localContext.setAttribute("preemptive-auth", basicAuth);

            httpClient.addRequestInterceptor(new PreemptiveAuth(proxy), 0);
            httpClient.setProxyAuthenticationHandler(ProxyAuthHandler.getInstance());
        }
    }

    private long generateId(ConnectionKey key) {
        long hash = key.baseUrl != null ? key.baseUrl.hashCode() : 0;
        hash = 31 * hash + (key.reverseGeocodingUrl != null ? key.reverseGeocodingUrl.hashCode() : 0);
        hash = 31 * hash + (key.routingUrl != null ? key.routingUrl.hashCode() : 0);
        hash = 31 * hash + (key.geocodingUrl != null ? key.geocodingUrl.hashCode() : 0);
        hash = 31 * hash + (key.wsdlLocation != null ? key.wsdlLocation.hashCode() : 0);
        return hash << 32 | ((int) (key.cid ^ (key.cid >>> 32)));
    }

    public IGeoRouting getGeoRoutingProvider(long connectionId) {
        return (IGeoRouting) connections.get(connectionId);
    }

    @PostConstruct
    public void init() {
        log = new Logger(getClass());
        log.info("STARTING SERVICE...");
        connections = new HashMap<Long, Object>();
        clients = new HashMap<ConnectionKey, GeoInformationClient>();
    }

    @PreDestroy
    public void release() {
        log.info("STOPING SERVICE...");
        //TODO: add disconnection logic.
        connections.clear();
        clients.clear();
    }

    class ConnectionKey {

        private final long cid;
        private final String baseUrl;
        private final String reverseGeocodingUrl;
        private final String routingUrl;
        private final String geocodingUrl;
        private final String wsdlLocation;

        public ConnectionKey(long cid, @Nullable String baseUrl, @Nullable String reverseGeocodingUrl,
                             @Nullable String routingUrl, @Nullable String geocodingUrl, @Nullable String wsdlLocation) {
            this.cid = cid;
            this.baseUrl = baseUrl;
            this.reverseGeocodingUrl = reverseGeocodingUrl;
            this.routingUrl = routingUrl;
            this.geocodingUrl = geocodingUrl;
            this.wsdlLocation = wsdlLocation;
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof ConnectionKey)) {
                return false;
            }
            final ConnectionKey that = (ConnectionKey) o;
            return cid == that.cid
                    && !(baseUrl != null ? !baseUrl.equals(that.baseUrl) : that.baseUrl != null)
                    && !(geocodingUrl != null ? !geocodingUrl.equals(that.geocodingUrl) : that.geocodingUrl != null)
                    && !(reverseGeocodingUrl != null ? !reverseGeocodingUrl.equals(that.reverseGeocodingUrl) : that.reverseGeocodingUrl != null)
                    && !(routingUrl != null ? !routingUrl.equals(that.routingUrl) : that.routingUrl != null)
                    && !(wsdlLocation != null ? !wsdlLocation.equals(that.wsdlLocation) : that.wsdlLocation != null);
        }

        @Override
        public int hashCode() {
            int result = (int) (cid ^ (cid >>> 32));
            result = 31 * result + (baseUrl != null ? baseUrl.hashCode() : 0);
            result = 31 * result + (reverseGeocodingUrl != null ? reverseGeocodingUrl.hashCode() : 0);
            result = 31 * result + (routingUrl != null ? routingUrl.hashCode() : 0);
            result = 31 * result + (geocodingUrl != null ? geocodingUrl.hashCode() : 0);
            result = 31 * result + (wsdlLocation != null ? wsdlLocation.hashCode() : 0);
            return result;
        }
    }

    //TODO: USE AuthCache INSTEAD THIS CONSTRUCTION.
    public class PreemptiveAuth implements HttpRequestInterceptor, Serializable {

        static final long serialVersionUID = 8743935542956230247L;

        final private HttpHost proxy;

        public PreemptiveAuth(HttpHost proxy) {
            this.proxy = proxy;
        }

        public void process(
                final HttpRequest request,
                final HttpContext context) throws HttpException, IOException {

            AuthState authState = (AuthState) context.getAttribute(
                    ClientContext.TARGET_AUTH_STATE);
            //AuthState authState = (AuthState) context.getAttribute(
            //        ClientContext.PROXY_AUTH_STATE);

            // If no auth scheme avaialble yet, try to initialize it preemptively
            if (authState.getAuthScheme() == null) {
                BasicScheme basicAuth = new BasicScheme();
                context.setAttribute("preemptive-auth", basicAuth);
                AuthScheme authScheme = (AuthScheme) context.getAttribute(
                        "preemptive-auth");
                CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(
                        ClientContext.CREDS_PROVIDER);
                if (authScheme != null) {
                    Credentials creds = credsProvider.getCredentials(
                            new AuthScope(
                                    proxy.getHostName(),
                                    proxy.getPort()));
                    if (creds == null) {
                        throw new HttpException("No credentials for preemptive authentication");
                    }
                    authState.setAuthScheme(authScheme);
                    authState.setCredentials(creds);
                }
            }
        }
    }

    class ProxyCredentials implements Credentials, Serializable {

        static final long serialVersionUID = 7394700206564651737L;

        final private Principal principal;
        final private String password;

        ProxyCredentials(String proxyLogin, ConnectionCredentials creds) {
            this.principal = creds != null && creds.getUserPrincipal() != null ? creds.getUserPrincipal() :
                    new ProxyUserPrincipal(proxyLogin);
            this.password = creds != null ? creds.getPassword() : null;
        }

        public Principal getUserPrincipal() {
            return principal;
        }

        public String getPassword() {
            return password;
        }
    }

    private class ProxyUserPrincipal implements Principal, Serializable {

        static final long serialVersionUID = 6384794004671572152L;

        private final String name;

        public ProxyUserPrincipal(String proxyLogin) {
            this.name = proxyLogin;
        }

        public String getName() {
            return name;
        }
    }

    @Override
    public IGeocoding getGeocodingProvider(long connectionId) {
        return (IGeocoding) connections.get(connectionId);
    }

    @Override
    public IReverseGeocoding getReverseGeocodingProvider(long connectionId) {
        return (IReverseGeocoding) connections.get(connectionId);
    }
}